<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>AI 对话模式 - WebIM UI 主题演示</title>
    <link href="./layui/css/layui.css" rel="stylesheet">
    <link href="./css/demo.css" rel="stylesheet">
  </head>
<body>

<div class="layui-panel demo-menu">
  <h1>WebIM UI</h1>
  <ul class="layui-menu" id="demo-menu">
    <li class="layui-menu-item-divider"></li>
    <li>
      <div class="layui-menu-body-title">
        <a href="./">综合演示</a>
      </div>
    </li>
    <li class="layui-menu-item-checked">
      <div class="layui-menu-body-title">
        <a href="./ai.html">AI 对话模式 <span class="layui-badge-dot"></span></a>
      </div>
    </li>
    <li>
      <div class="layui-menu-body-title">
        <a href="./mobile.html" target="_blank">WAP 版演示</a>
      </div>
    </li>
  </ul>
</div>

<div class="layui-container">
  <fieldset class="layui-elem-field layui-field-title" style="margin-top: 7px;">
    <legend>设置</legend>
  </fieldset>
  <div class="layui-form">
    <div class="layui-form-item">
      <label class="layui-form-label">测试方式</label>
      <div class="layui-input-block">
        <input type="radio" name="testMode" value="requestDemo" title="纯静态模拟" lay-filter="testMode" checked>
        <input type="radio" name="testMode" value="requestQwen" title="阿里云「通义千问」系列大模型" lay-filter="testMode">
      </div>
    </div>
    <div class="layui-form-item layui-text demo-test-tabs-body">
      <div set="requestDemo"></div>
      <div set="requestQwen">
        <blockquote class="layui-elem-quote" style="margin-bottom: 24px;">
          <strong>⚡ 请注意：</strong>
          <ol>
            <li>
              测试阿里云「通义千问」部分模型将产生计费，具体可参考：
              1. <a href="https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-thousand-questions-metering-and-billing#TeYcd" target="_blank">
                通义千问计费说明
              </a>
              2. <a href="https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-qianwen-7b-14b-72b-metering-and-billing#TeYcd" target="_blank">
                通义千问开源系列计费说明
              </a>
            </li>
            <li>
              为了便于本地演示，我们在浏览器端直接请求 AI 平台接口，但安全模式下会存在跨域限制。因此，您需要创建一个 Chrome 快捷方式，并右键属性，在目标中追加以下参数启动：<code>--disable-web-security --user-data-dir=&lt;path_to_your_profile&gt;</code>，其中 <code>&lt;path_to_your_profile&gt;</code> 即 Chrome 所在的 AppData 目录。
            </li>
            <li>
              实际开发时，请务必将请求大模型的接口代码写在后端，避免在浏览器端直接暴露 API_KEY, SECRET_KEY。
            </li>
          </ol>
        </blockquote>
        <div class="layui-form-item">
          <label class="layui-form-label">API_KEY</label>
          <div class="layui-input-inline">
            <div class="layui-input-wrap">
              <input type="password" lay-affix="eye" value="" autocomplete="off" id="QWEN_API_KEY" class="layui-input">
            </div>
          </div>
          <div class="layui-form-mid">
            <i class="layui-icon layui-icon-tips"></i> 阿里云 <a href="https://dashscope.console.aliyun.com/overview" target="_blank">DashScope</a> 平台获取
          </div>
        </div>
        <div class="layui-form-item">
          <label class="layui-form-label">选择模型</label>
          <div class="layui-input-inline">
            <select id="QWEN_MODEL">
              <option value="qwen2-1.5b-instruct">qwen2-1.5b-instruct</option>
              <option value="qwen2-57b-a14b-instruct">qwen2-57b-a14b-instruct</option>
              <option value="qwen2-72b-instruct">qwen2-72b-instruct (核心)</option>
              <option value="qwen-turbo">qwen-turbo</option>
              <option value="qwen-max">qwen-max (核心)</option>
            </select>
          </div>
          <div class="layui-form-mid">
            <i class="layui-icon layui-icon-tips"></i> 模型具体介绍可参考：<a href="https://help.aliyun.com/zh/dashscope/developer-reference/model-square/" target="_blank">阿里云通义千问相关文档</a>
          </div>
        </div>
        <div class="layui-form-item">
          <label class="layui-form-label">交互方式</label>
          <div class="layui-input-inline">
            <select id="QWEN_REQUEST_MODE">
              <option value="stream">Fetch API 事件流（SSE）</option>
              <option value="ajax">jQuery Ajax</option>
            </select>
          </div>
        </div>
      </div>
    </div>
  </div>

  <fieldset class="layui-elem-field layui-field-title" style="margin: 32px 0;">
    <legend>测试</legend>
  </fieldset>
  <div class="layui-btn-container">
    <button type="button" class="layui-btn layui-btn-primary layui-border-blue" lay-on="normal">
      打开普通 AI 对话面板
    </button>
    <button type="button" class="layui-btn layui-btn-primary layui-border-blue" lay-on="max">
      🔥 打开最大化 AI 对话面板
    </button>
    <button type="button" class="layui-btn layui-btn-primary layui-border-blue" lay-on="random">
      😼 创建随机 AI 对话
    </button>
  </div>
</div>

<script>
  if(!/^http(s*):\/\//.test(location.href)) alert('请通过 localhost 访问该页面');
</script>

<script src="./layui/layui.js"></script>

<!-- 引入外部公共 CDN 的依赖库（用于演示），亦可下载到本地引入 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.2/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@sentool/fetch-event-source/dist/index.min.js"></script>

<script type="module">
  const { fetchEventSource } = FetchEventSource;

  // 加载核心组件
  layui.config({
    layimPath: '../dist/', // 配置 layim.js 所在目录
    layimResPath: '../dist/res/', // layim 资源文件所在目录
    version: '4.0.0'
  }).extend({
    layim: layui.cache.layimPath + 'layim' //  配置 layim 组件所在的路径
  }).use('layim', function(layim) {
    // 基础配置
    layim.config({
      contactsPanel: false, // 不开启联系人面板
      init: {
        user: { // 配置当前用户信息
          "username": "测试者", // 我的昵称
          "id": "100000" // 我的 ID
        }
      }
    });

    // 扩展聊天工具栏
    layim.extendChatTools([
      {
        name: 'shortcut',
        title: '快捷语',
        icon: 'layui-icon-list',
        onClick: function(obj) {
          layui.dropdown.render({
            elem: obj.elem,
            data: [
              {title: '写一篇 50 字左右的正能量小说'},
              {title: '写一篇 300 字左右的科幻小说'},
              {title: '写一篇 1000 字左右的历史小说'},
              {title: 'Layui table 的详细用法'},
              {title: '1+1=?'}
            ],
            show: true,
            click: function(data) {
              // obj.insert(data.title); // 将文本插入到编辑器
              layim.sendMessage(data.title) // 直接发送消息
            }
          })
        }
      }
    ]);

    /**
     * 自定义内容解析器
     * AI 一般返回 markdown 格式的内容，您可借助第三方 markdown 解析器渲染 AI 返回的内容
     * 常用的 markdown 解析器有：marked, markdown-it
     *
     * 请注意：
     * 1. markdown 解析器应禁用 HTML 标签（通常为默认），避免 XSS，确保返回安全的 HTML 内容
     * 2. 若不采用 markdown 解析器，请务必自行转义 XSS 字符
     *
     * 下面以使用 markdown-it 为例
     * @see https://markdown-it.github.io/markdown-it/value
     */
    layim.callback('contentParser', content => {
      const md = markdownit({
        typographer: true,
        linkify: true,
        breaks: true
      });
      return md.render(content); // 将 markdown 字符渲染成 HTML
    });

    // 事件 - 发送消息
    layim.on('sendMessage', function(data) {
      const user = data.user; // 我
      const receiver = data.receiver; // 对方
      console.log(data)

      // AI 对话模式
      if (receiver.type === 'ai') {
        async function render() {
          // 模拟演示 AI 接口，实际使用时可将下述 setTimeout 换成 Ajax, Fetch API, EventSource 的任意一种
          async function requestDemo (callback) {
            setTimeout(() => {
              callback('Hi，这是一条静态模拟的 `AI` 回复内容。\n\n- 实际使用时可采用 Ajax, Fetch API, EventSource 等任一方式获取对话内容。\n- 大模型 API 接入可参考「通义千问、百度智能云」等平台的相关文档。');
            }, 3000);
          };

          /**
           * 以下分别演示采用「事件流」和「jQuery Ajax」方式请求阿里云「通义千问」接口
           * 阿里「通义千问」@see https://help.aliyun.com/zh/dashscope/developer-reference/
           * 百度「千帆大模型」@see https://cloud.baidu.com/doc/WENXINWORKSHOP/s/6ltgkzya5
           *
           * 请注意 ⚡：
           *   1. 以下代码仅用于演示，请勿直接用于生产环境。
           *   2. 实际开发时，请务必将请求大模型的接口代码写在后端，避免在浏览器端直接暴露 API_KEY, SECRET_KEY。
           */
          async function requestQwen (callback) {
            const API_URL = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
            const API_KEY = document.querySelector('#QWEN_API_KEY').value;
            const API_MODEL = document.querySelector('#QWEN_MODEL').value;

            // 交互方式
            const QWEN_REQUEST_MODE = document.querySelector('#QWEN_REQUEST_MODE').value;

            // 基础参数
            const options = {
              method: 'POST',
              headers: {
                "Authorization": `Bearer ${API_KEY}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({
                model: API_MODEL, // 模型
                input: {
                  messages: messages[rid]
                },
                parameters: {
                  temperature: 1.3,
                  top_p: 0.9,
                  max_tokens: 1500,
                }
              })
            };

            // 事件流交互方式
            if (QWEN_REQUEST_MODE === 'stream') {
              const eventSource = await fetchEventSource(API_URL, {
                ...options,
                onopen(response) {
                  if (!response.ok) {
                    throw new Error(`URL ${response.status} ${response.statusText}`)
                  }
                },
                onmessage(event) {
                  // console.log(event);
                  const { data, id } = event;
                  const { output }  = data;
                  const finished = output.finish_reason === 'stop';

                  let text = output.text + ((finished || id % 2) ? '' : ' _');
                  callback?.(text, finished);

                  // 将回复内容加入到多轮对话
                  if (finished) {
                    messages[rid]?.push({
                      role: 'assistant',
                      content: text
                    });
                  }
                },
                onerror(error) {
                  callback(`**Error**: ${error}`);
                }
              });
              // console.log(eventSource
            } else if (QWEN_REQUEST_MODE === 'ajax') { // 传统 jQuery Ajax 交互方式
              options.data = options.body;
              delete options.body;

              layui.$.ajax({
                ...options,
                url: API_URL,
                success(data) {
                  // console.log(data);
                  const { output }  = data;
                  const finished = output.finish_reason === 'stop';
                  let text = output.text;

                  callback?.(text, finished);

                  // 将回复内容加入到多轮对话
                  messages[rid]?.push({
                    role: 'assistant',
                    content: text
                  });
                },
                error(error) {
                  callback(`**Error**: ${error.responseText}`);
                }
              });
            }

          }

          // 多轮对话
          const rid = receiver.id;
          const messages = layim.chat.messages = layim.chat.messages || {
            [rid]: []
          };
          messages[rid] = messages[rid] || []
          messages[rid]?.push({
            role: 'user',
            content: user.content
          });

          // 测试方式
          const testMode = document.querySelector('[name="testMode"]:checked').value;
          const requests = { requestDemo };
          typeof requestQwen === 'function' && (requests.requestQwen = requestQwen);

          // 获得 AI 回复内容
          requests[testMode]((content, finished) => {
            // layim 接受对话消息
            layim.getMessage({
              ...receiver,
              content: content,
              finished
            });
          });
        }

        return render();
      }
    });

    // 聊天面板初始打开的事件
    layim.on('chatInit', function(obj) {
      const data = obj.data;
      const elem = obj.elem;
      if (data.type === 'ai') {
        // 每次刷新页面，即提示为新的对话
        const lastItem = elem.find('.layim-chat-main>ul>li').last();
        const existMessages = layim.chat.messages && layim.chat.messages[data.id];
        if (!existMessages && !lastItem.hasClass('layim-chat-system')) {
          layim.getMessage({
            system: true,
            id: data.id,
            type: 'ai',
            content: '以下为新的对话',
            // saveLocalChatlog: true // 是否将该消息保存到本地的聊天记录
          });
        }
      }
    });

    /**
     * 其他操作
     */
    const { $, util, form } = layui;

    // 创建 AI 对话
    const createAIChat = {
      // 普通 AI 对话面板
      normal() {
        layim.chat({
          username: 'AI 助手测试',
          type: 'ai', // AI 模式
          avatar: '', // 头像
          id: 1,
          new: true, // 始终创建新的聊天面板
          layer: {
            maxmin: false // 不显示最大/小化图标
          }
        });
      },
      // 最大化 AI 对话面板
      max({username = 'AI 助手测试', id = 1, enableLocalChatlog = true} = {}) {
        layim.chat({
          username: username,
          type: 'ai', // AI 模式
          avatar: '', // 头像
          id: id,
          new: true, // 始终创建新的聊天面板
          enableLocalChatlog,
          layer: {
            move: false,
            offset: 'r',
            maxmin: false
          },
          success(layero, index) {
            // 重置弹层样式
            layer.style(index, {
              top: 0,
              left: '151px', // 同侧边菜单占用的宽度
              right: 0,
              width: 'auto',
              height: '100%'
            }, true);
            // 让聊天内容区域高度自适应
            layero.find('.layim-chat-main').css({
              height: 'calc(100vh - 202px)' // 此处 202 为标题和底部输入框占用的高度
            })
          }
        });
      }
    };

    // 面板外的操作示例事件
    util.on({
      ...createAIChat,
      max() {
        createAIChat.max()
      },
      // 创建随机 AI 对话
      random() {
        const id = Math.floor(Math.random()*99999999); // 生成随机 id
        createAIChat.max({
          username: `AI 助手测试@${id}`,
          id,
          enableLocalChatlog: false // 不启用本地聊天记录
        });
      }
    });

    // 测试方式 - radio 事件
    form.on('radio(testMode)', function(data) {
      var elem = data.elem; // 获得 radio 原始 DOM 对象
      var value = elem.value; // 获得 radio 值
      $(`.demo-test-tabs-body>[set="${value}"]`).show().siblings().hide();
    })
  });
</script>
</body>
</html>
